/*

  MMetaverseSurface

  Copyright (c) 2009 Mary Jane Soft - Marlon J. Manrique
	
  http://www.maryjanesoft.com
  http://www.marlonj.com

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.
 
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
 
  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

  $Id$
	
*/

using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Threading;

using OpenMetaverse;

namespace mjs.mmetaverse.surface
{
	/// <summary>
	/// MMetaverse Surface application that control avatars inside OpenSim or 
	/// Second Life using a surface using the reacTIVision application  
	/// </summary>
	public class MMetaverseSurface
	{				
		/// <summary>
		/// Simulator Authentication URL 
		/// </summary>
		private string loginURI; 

		/// <summary>
		/// Coordinate X of the region where the avatar can move
		/// </summary>
		private float landX;

		/// <summary>
		/// Coordinate Y of the region where the avatar can move
		/// </summary>
		private float landY;

		/// <summary>
		/// Width of the land where the avatar can move 
		/// </summary>
		private float landWidth;

		/// <summary>
		/// Height of the land where the avatar can move 
		/// </summary>
		private float landHeight;

		/// <summary>
		/// Current list of avatars 
		/// </summary>
		private Dictionary<int,AvatarInfo> avatars;

		/// <summary>
		/// Current List of clients connected to the simulator
		/// </summary>
		private Dictionary<int,GridClient> clients;

		/// <summary>
		/// Current List of objects to control 
		/// </summary>
		private Dictionary<int,RemoteObject> objects;

		/// <summary>
		/// Surface listener to catch events from the surface 
		/// </summary>
		private SurfaceListener surfaceListener;

		/// <summary>
		/// Create a new surface 
		/// </summary>
		public MMetaverseSurface()
		{
			// Create the avatars and network clients list 
			avatars = new Dictionary<int,AvatarInfo>();
			clients = new Dictionary<int,GridClient>();
			objects = new Dictionary<int,RemoteObject>();

			// Create a new surface listener to read data from the reacTIVision 
			surfaceListener = new SurfaceListener(this);

			// Load the configuration of the application 
			loadConfiguration();
		}

		/// <summary>
		/// Load the configuration of the application. Inside the application 
		/// configuration must exists the LoginURI with the url use to 
		/// authenticate the avatars  
		/// </summary>
		private void loadConfiguration() 
		{
			// Read the login url from the applicatoin configuration
			loginURI = ConfigurationManager.AppSettings["LoginURI"];

			// Read the land values where the avatar can be move 
			landX = float.Parse(ConfigurationManager.AppSettings["LandX"]);
			landY = float.Parse(ConfigurationManager.AppSettings["LandY"]);
			landWidth = float.Parse(ConfigurationManager.AppSettings["LandWidth"]);
			landHeight = float.Parse(ConfigurationManager.AppSettings["LandHeight"]);

			// Load the avatar list from the avatars.txt file 
			loadAvatarList();

			// Load the object list from the objects.txt file 
			loadObjectList();

			// Open the clients with the simulator 
			createClients();
		}	
		
		/// <summary>
		/// Load the avatar list from the avatars.txt file. 
		/// Each line contains the fiducial asigned to the avatar, the names 
		/// and the password 
		/// </summary>
		private void loadAvatarList() 
		{
			// Split the line using comma 
			char[] delim = { ',' };

			// Open the avatar list file 
			StreamReader sr = new StreamReader("avatars.txt");
			
			// Read each line and split the information			
			string line;
			while((line= sr.ReadLine()) != null)
			{
				// Get the data on the line 
				string[] tokens = line.Split(delim);
				
				// Add a new avatar with the information found 
				addAvatar(Convert.ToInt32(tokens[0]),
					tokens[1],tokens[2],tokens[3]);
			}
		}

		/// <summary>
		/// Load the object list from the objects.txt file. 
		/// Each line contains the fiducial asigned to the object, and 
		/// the remote channel id assigned to the object
		/// </summary>
		private void loadObjectList() 
		{
			// Split the line using comma 
			char[] delim = { ',' };

			// Open the avatar list file 
			StreamReader sr = new StreamReader("objects.txt");
			
			// Read each line and split the information			
			string line;
			while((line= sr.ReadLine()) != null)
			{
				// Get the data on the line 
				string[] tokens = line.Split(delim);
				
				// Add a new avatar with the information found 
				addObject(Convert.ToInt32(tokens[0]),
					tokens[1]);
			}
		}
		
		/// <summary>
		/// Add a new avatar to the surface, using the information specified to 
		/// control and authenticate inside the simulator.
		/// </summary>
		/// <param name="id">
		/// The fiducial ID use in the reacTIVision application. 
		/// <see cref="System.Int32"/>
		/// </param>
		/// <param name="firstname">
		/// The first name of the avatar <see cref="System.String"/>
		/// </param>
		/// <param name="lastname">
		/// The last name of the avatar  <see cref="System.String"/>
		/// </param>
		/// <param name="password">
		/// The plain password of the avatar <see cref="System.String"/>
		/// </param>
		public void addAvatar(int id, string firstname, string lastname, string password) 
		{
			// Create a new avatar info object and add to the list 
			// use the fiducial id to find quickly the avatar 
			AvatarInfo avatar = new AvatarInfo(id,firstname,lastname,password);
			avatars.Add(id,avatar);
		}

		/// <summary>
		/// Add a new network client to the surface asociated with the fiducial id. 
		/// </summary>
		/// <param name="id">
		/// Fiducial ID asociated with the avatar. <see cref="System.Int32"/>
		/// </param>
		/// <param name="gridClient">
		/// The network client. <see cref="GridClient"/>
		/// </param>
		public void addClient(int id, GridClient gridClient)
		{
			// Add the client to the dictionary 
			clients.Add(id,gridClient);

			// Set Appearance 
			setAppearance(gridClient);

			// Move the avatar to the center of the land
			moveAvatarTo(id,0.5f,0.5f);
		}

		/// <summary>
		/// Add a new object to the surface, using the information specified to 
		/// control the location and rotation of the object.
		/// </summary>
		/// <param name="id">
		/// The fiducial ID use in the reacTIVision application. 
		/// <see cref="System.Int32"/>
		/// </param>
		/// <param name="channelID">
		/// The remote channel id <see cref="System.String"/>
		/// </param>
		public void addObject(int id, string channelID) 
		{
			// Create a new remote object and add to the list 
			// use the fiducial id to find quickly the object
			RemoteObject remoteObject = new RemoteObject(id,channelID,getXmlRpcURL());
			objects.Add(id,remoteObject);
		}

		/// <summary>
		/// Set the appearance for the client. 
		/// </summary>
		/// <param name="gridClient">
		/// The network client. <see cref="GridClient"/>
		/// </param>
		private void setAppearance(GridClient gridClient)
		{
			// Event success 
			bool success = false;

			// Use to notify the waiting thread 
			AutoResetEvent appearanceEvent = new AutoResetEvent(false);

			// Stop thread when appearance available 
			AppearanceManager.AppearanceUpdatedCallback callback =
				delegate(Primitive.TextureEntry te) { appearanceEvent.Set(); };

			// Set event 
			gridClient.Appearance.OnAppearanceUpdated += callback;

			// The client show a cloud avatar so we need to set the last appearance
			gridClient.Appearance.SetPreviousAppearance(false);

			// Wait for the process to complete or time out
			if(appearanceEvent.WaitOne(1000 * 120, false))
				success = true;

			// Unregister the handler
			gridClient.Appearance.OnAppearanceUpdated -= callback;

			// Check status 
			if(!success)
				System.Console.WriteLine("Unable to ser appearance");			
		}

		/// <summary>
		/// Create a new network client connected to the simulator. 
		/// </summary>
		/// <param name="id">
		/// The fiducial id associated with the avatar. <see cref="System.Int32"/>
		/// </param>
		/// <param name="firstname">
		/// The first name of the avatar <see cref="System.String"/>
		/// </param>
		/// <param name="lastname">
		/// The last name of the avatar  <see cref="System.String"/>
		/// </param>
		/// <param name="password">
		/// The plain password of the avatar <see cref="System.String"/>
		/// </param>
		public void createClient(int id, string firstname, string lastname, string password) 
		{
			// Create a new Client 
			GridClient client = new GridClient();

			// Set Login parameters
			LoginParams loginParams = 
				client.Network.DefaultLoginParams(
					firstname,lastname,password,"MMetaverseSurface","0.1");

			// Set login uri 
			loginParams.URI = loginURI;

			// Do Login 
			if(client.Network.Login(loginParams)) 
				addClient(id,client);
			else
				System.Console.WriteLine("Unable to Login");
		}

		/// <summary>
		/// Rotate the entity associated with the fiducial id, to the specified angle. 
		/// </summary>
		/// <param name="id">
		/// The fiducial id associated with the entity. <see cref="System.Int32"/>
		/// </param>
		/// <param name="angle">
		/// The angle in radians to rotate. <see cref="System.Single"/>
		/// </param>
		public void rotate(int id, float angle)
		{
			// Check if the id is an avatar or an object
			// And send the message to the simulator
			if(avatars.ContainsKey(id))
				rotateAvatar(id,angle);
			else
				rotateObject(id,angle);
		}

		/// <summary>
		/// Move the entity to the location specified inside the region.
		/// </summary>
		/// <param name="id">
		/// The fiducial id associated with the entity. <see cref="System.Int32"/>
		/// </param>
		/// <param name="x">
		/// The x coordinate inside the surface, between 0 and 1. <see cref="System.Single"/>
		/// </param>
		/// <param name="y">
		/// The y coordinate inside the surface, between 0 and 1. <see cref="System.Single"/>
		/// </param>
		public void move(int id, float x, float y)
		{
			// Check if the id is an avatar or an object
			// And send the message to the simulator
			if(avatars.ContainsKey(id))
				moveAvatarTo(id,x,y);
			else
				moveObjectTo(id,x,y);
		}

		/// <summary>
		/// Rotate the avatar associated with the fiducial id, to the specified angle. 
		/// </summary>
		/// <param name="id">
		/// The fiducial id associated with the avatar. <see cref="System.Int32"/>
		/// </param>
		/// <param name="angle">
		/// The angle in radians to rotate. <see cref="System.Single"/>
		/// </param>
		public void rotateAvatar(int id, float angle)
		{
			// Send the request to the simulator 
			clients[id].Self.Movement.UpdateFromHeading(angle, false);
		}

		/// <summary>
		/// Move the avatar to the location specified inside the region.
		/// </summary>
		/// <param name="id">
		/// The fiducial id associated with the avatar. <see cref="System.Int32"/>
		/// </param>
		/// <param name="x">
		/// The x coordinate inside the surface, between 0 and 1. <see cref="System.Single"/>
		/// </param>
		/// <param name="y">
		/// The y coordinate inside the surface, between 0 and 1. <see cref="System.Single"/>
		/// </param>
		public void moveAvatarTo(int id, float x, float y)
		{
			// Get region location	
			uint regionX, regionY;
			Utils.LongToUInts(clients[id].Network.CurrentSim.Handle, out regionX, out regionY);

			// Add the relative position inside the land 
			// and add the land initial point
			// and the region initial point relative to surface position 
			x = regionX + landX + landWidth * x;
			y = regionY + landY + landHeight * y;

			// Update location 
			// Use autopilot to allow the avatar walk inside the region 
			clients[id].Self.AutoPilot(x, y, 23);
		}

		/// <summary>
		/// Rotate the object associated with the fiducial id, to the specified angle. 
		/// </summary>
		/// <param name="id">
		/// The fiducial id associated with a object. <see cref="System.Int32"/>
		/// </param>
		/// <param name="angle">
		/// The angle in radians to rotate. <see cref="System.Single"/>
		/// </param>
		public void rotateObject(int id, float angle)
		{
			// Send the request to the remote object
			objects[id].rotate(angle);
		}

		/// <summary>
		/// Move the object to the location specified inside the region.
		/// </summary>
		/// <param name="id">
		/// The fiducial id associated with the object. <see cref="System.Int32"/>
		/// </param>
		/// <param name="x">
		/// The x coordinate inside the surface, between 0 and 1. <see cref="System.Single"/>
		/// </param>
		/// <param name="y">
		/// The y coordinate inside the surface, between 0 and 1. <see cref="System.Single"/>
		/// </param>
		public void moveObjectTo(int id, float x, float y)
		{
			// Add the relative position inside the land 
			// and add the land initial point
			// and the region initial point relative to surface position 
			x = landX + landWidth * x;
			y = landY + landHeight * y;

			// Update location 
			objects[id].move(x, y, 23);
		}

		/// <summary>
		/// Open the connection of the avatars specified in the list of avatars. 
		/// </summary>
		public void createClients() 
		{
			// Open a network connection with the simulator for each client 
			foreach(var avatar in avatars)
			{
				createClient(avatar.Value.getFiducialID(),
					avatar.Value.getFirstname(),avatar.Value.getLastname(),avatar.Value.getPassword());
			}
		}

		/// <summary>
		/// Return the XML-RPC server url according the simulator.
		/// </summary>
		/// <returns>
		/// The url to send the request. <see cref="System.String"/>
		/// </returns>	
		public string getXmlRpcURL()
		{
			// Check if login url is second life 
			// and set the xmlrpc url 
			if(loginURI.StartsWith("https://login.agni.lindenlab.com"))
				return "http://xmlrpc.secondlife.com/cgi-bin/xmlrpc.cgi";
			else
				return loginURI + "/cgi-bin/xmlrpc.cgi";
		}

		/// <summary>
		/// Close the network clients. 
		/// </summary>
		public void logoutClients() 
		{
			// Close the network clients 
			foreach(var client in clients)
				client.Value.Network.Logout();
		}

		/// <summary>
		/// Close the application 
		/// </summary>
		public void close() 
		{
			// Stop listening reacTIVision events 
			surfaceListener.disconnect();
			
			// Close all network clients 
			logoutClients();
		}

		/// <summary>
		/// Create a new surface. 
		/// </summary>
		public static void Main() 
		{
			// Create a new surface 
			MMetaverseSurface surface = new MMetaverseSurface();			
			
			// Wait until a key is pressed 
			System.Console.Read();

			// Close the application  
			surface.close();
		}
	}
}
